/*
 * WPA Supplicant - driver interaction with old Broadcom wl.o driver
 * Copyright (c) 2004, Nikki Chumkov <nikki@gattaca.ru>
 * Copyright (c) 2004, Jouni Malinen <j@w1.fi>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * Alternatively, this software may be distributed under the terms of BSD
 * license.
 *
 * See README and COPYING for more details.
 *
 * Please note that the newer Broadcom driver ("hybrid Linux driver") supports
 * Linux wireless extensions and does not need (or even work) with this old
 * driver wrapper. Use driver_wext.c with that driver.
 */

#include "includes.h"

#include <sys/ioctl.h>

#include "common.h"

#if 0
#include <netpacket/packet.h>
#include <net/ethernet.h>     /* the L2 protocols */
#else
#include <linux/if_packet.h>
#include <linux/if_ether.h>   /* The L2 protocols */
#endif
#include <net/if.h>
#include <typedefs.h>

/* wlioctl.h is a Broadcom header file and it is available, e.g., from Linksys
 * WRT54G GPL tarball. */
#include <wlioctl.h>

#include "driver.h"
#include "eloop.h"

struct wpa_driver_broadcom_data {
        void *ctx;
        int ioctl_sock;
        int event_sock;
        char ifname[IFNAMSIZ + 1];
};


#ifndef WLC_DEAUTHENTICATE
#define WLC_DEAUTHENTICATE 143
#endif
#ifndef WLC_DEAUTHENTICATE_WITH_REASON
#define WLC_DEAUTHENTICATE_WITH_REASON 201
#endif
#ifndef WLC_SET_TKIP_COUNTERMEASURES
#define WLC_SET_TKIP_COUNTERMEASURES 202
#endif

#if !defined(PSK_ENABLED) /* NEW driver interface */
#define WL_VERSION 360130
/* wireless authentication bit vector */
#define WPA_ENABLED 1
#define PSK_ENABLED 2
                                                                                
#define WAUTH_WPA_ENABLED(wauth)  ((wauth) & WPA_ENABLED)
#define WAUTH_PSK_ENABLED(wauth)  ((wauth) & PSK_ENABLED)
#define WAUTH_ENABLED(wauth)    ((wauth) & (WPA_ENABLED | PSK_ENABLED))

#define WSEC_PRIMARY_KEY WL_PRIMARY_KEY

typedef wl_wsec_key_t wsec_key_t;
#endif

typedef struct {
        uint32 val;
        struct ether_addr ea;
        uint16 res;
} wlc_deauth_t;


static void wpa_driver_broadcom_scan_timeout(void *eloop_ctx,
                                             void *timeout_ctx);

static int broadcom_ioctl(struct wpa_driver_broadcom_data *drv, int cmd,
                          void *buf, int len)
{
        struct ifreq ifr;
        wl_ioctl_t ioc;
        int ret = 0;

        wpa_printf(MSG_MSGDUMP, "BROADCOM: wlioctl(%s,%d,len=%d,val=%p)",
                   drv->ifname, cmd, len, buf);
        /* wpa_hexdump(MSG_MSGDUMP, "BROADCOM: wlioctl buf", buf, len); */

        ioc.cmd = cmd;
        ioc.buf = buf;
        ioc.len = len;
        os_strlcpy(ifr.ifr_name, drv->ifname, IFNAMSIZ);
        ifr.ifr_data = (caddr_t) &ioc;
        if ((ret = ioctl(drv->ioctl_sock, SIOCDEVPRIVATE, &ifr)) < 0) {
                if (cmd != WLC_GET_MAGIC)
                        perror(ifr.ifr_name);
                wpa_printf(MSG_MSGDUMP, "BROADCOM: wlioctl cmd=%d res=%d",
                           cmd, ret);
        }

        return ret;
}

static int wpa_driver_broadcom_get_bssid(void *priv, u8 *bssid)
{
        struct wpa_driver_broadcom_data *drv = priv;
        if (broadcom_ioctl(drv, WLC_GET_BSSID, bssid, ETH_ALEN) == 0)
                return 0;
        
        os_memset(bssid, 0, ETH_ALEN);
        return -1;
}

static int wpa_driver_broadcom_get_ssid(void *priv, u8 *ssid)
{
        struct wpa_driver_broadcom_data *drv = priv;
        wlc_ssid_t s;
        
        if (broadcom_ioctl(drv, WLC_GET_SSID, &s, sizeof(s)) == -1)
                return -1;

        os_memcpy(ssid, s.SSID, s.SSID_len);
        return s.SSID_len;
}

static int wpa_driver_broadcom_set_wpa(void *priv, int enable)
{
        struct wpa_driver_broadcom_data *drv = priv;
        unsigned int wauth, wsec;
        struct ether_addr ea;

        os_memset(&ea, enable ? 0xff : 0, sizeof(ea));
        if (broadcom_ioctl(drv, WLC_GET_WPA_AUTH, &wauth, sizeof(wauth)) ==
            -1 ||
            broadcom_ioctl(drv, WLC_GET_WSEC, &wsec, sizeof(wsec)) == -1)
                return -1;

        if (enable) {
                wauth = PSK_ENABLED;
                wsec = TKIP_ENABLED;
        } else {
                wauth = 255;
                wsec &= ~(TKIP_ENABLED | AES_ENABLED);
        }

        if (broadcom_ioctl(drv, WLC_SET_WPA_AUTH, &wauth, sizeof(wauth)) ==
            -1 ||
            broadcom_ioctl(drv, WLC_SET_WSEC, &wsec, sizeof(wsec)) == -1)
                return -1;

        /* FIX: magic number / error handling? */
        broadcom_ioctl(drv, 122, &ea, sizeof(ea));

        return 0;
}

static int wpa_driver_broadcom_set_key(void *priv, wpa_alg alg,
                                       const u8 *addr, int key_idx, int set_tx,
                                       const u8 *seq, size_t seq_len,
                                       const u8 *key, size_t key_len)
{
        struct wpa_driver_broadcom_data *drv = priv;
        int ret;
        wsec_key_t wkt;

        os_memset(&wkt, 0, sizeof wkt);
        wpa_printf(MSG_MSGDUMP, "BROADCOM: SET %sKEY[%d] alg=%d",
                   set_tx ? "PRIMARY " : "", key_idx, alg);
        if (key && key_len > 0)
                wpa_hexdump_key(MSG_MSGDUMP, "BROADCOM: key", key, key_len);

        switch (alg) {
        case WPA_ALG_NONE:
                wkt.algo = CRYPTO_ALGO_OFF;
                break;
        case WPA_ALG_WEP:
                wkt.algo = CRYPTO_ALGO_WEP128; /* CRYPTO_ALGO_WEP1? */
                break;
        case WPA_ALG_TKIP:
                wkt.algo = 0; /* CRYPTO_ALGO_TKIP? */
                break;
        case WPA_ALG_CCMP:
                wkt.algo = 0; /* CRYPTO_ALGO_AES_CCM;
                               * AES_OCB_MSDU, AES_OCB_MPDU? */
                break;
        default:
                wkt.algo = CRYPTO_ALGO_NALG;
                break;
        }

        if (seq && seq_len > 0)
                wpa_hexdump(MSG_MSGDUMP, "BROADCOM: SEQ", seq, seq_len);

        if (addr)
                wpa_hexdump(MSG_MSGDUMP, "BROADCOM: addr", addr, ETH_ALEN);

        wkt.index = key_idx;
        wkt.len = key_len;
        if (key && key_len > 0) {
                os_memcpy(wkt.data, key, key_len);
                if (key_len == 32) {
                        /* hack hack hack XXX */
                        os_memcpy(&wkt.data[16], &key[24], 8);
                        os_memcpy(&wkt.data[24], &key[16], 8);
                }
        }
        /* wkt.algo = CRYPTO_ALGO_...; */
        wkt.flags = set_tx ? 0 : WSEC_PRIMARY_KEY;
        if (addr && set_tx)
                os_memcpy(&wkt.ea, addr, sizeof(wkt.ea));
        ret = broadcom_ioctl(drv, WLC_SET_KEY, &wkt, sizeof(wkt));
        if (addr && set_tx) {
                /* FIX: magic number / error handling? */
                broadcom_ioctl(drv, 121, &wkt.ea, sizeof(wkt.ea));
        }
        return ret;
}


static void wpa_driver_broadcom_event_receive(int sock, void *ctx,
                                              void *sock_ctx)
{
        char buf[8192];
        int left;
        wl_wpa_header_t *wwh;
        union wpa_event_data data;
        
        if ((left = recv(sock, buf, sizeof buf, 0)) < 0)
                return;

        wpa_hexdump(MSG_DEBUG, "RECEIVE EVENT", (u8 *) buf, left);

        if ((size_t) left < sizeof(wl_wpa_header_t))
                return;

        wwh = (wl_wpa_header_t *) buf;

        if (wwh->snap.type != WL_WPA_ETHER_TYPE)
                return;
        if (os_memcmp(&wwh->snap, wl_wpa_snap_template, 6) != 0)
                return;

        os_memset(&data, 0, sizeof(data));

        switch (wwh->type) {
        case WLC_ASSOC_MSG:
                left -= WL_WPA_HEADER_LEN;
                wpa_printf(MSG_DEBUG, "BROADCOM: ASSOC MESSAGE (left: %d)",
                           left);
                if (left > 0) {
                        data.assoc_info.resp_ies = os_malloc(left);
                        if (data.assoc_info.resp_ies == NULL)
                                return;
                        os_memcpy(data.assoc_info.resp_ies,
                                  buf + WL_WPA_HEADER_LEN, left);
                        data.assoc_info.resp_ies_len = left;
                        wpa_hexdump(MSG_MSGDUMP, "BROADCOM: copying %d bytes "
                                    "into resp_ies",
                                    data.assoc_info.resp_ies, left);
                }
                /* data.assoc_info.req_ies = NULL; */
                /* data.assoc_info.req_ies_len = 0; */

                wpa_supplicant_event(ctx, EVENT_ASSOCINFO, &data);
                wpa_supplicant_event(ctx, EVENT_ASSOC, NULL);
                break;
        case WLC_DISASSOC_MSG:
                wpa_printf(MSG_DEBUG, "BROADCOM: DISASSOC MESSAGE");
                wpa_supplicant_event(ctx, EVENT_DISASSOC, NULL);
                break;
        case WLC_PTK_MIC_MSG:
                wpa_printf(MSG_DEBUG, "BROADCOM: PTK MIC MSG MESSAGE");
                data.michael_mic_failure.unicast = 1;
                wpa_supplicant_event(ctx, EVENT_MICHAEL_MIC_FAILURE, &data);
                break;
        case WLC_GTK_MIC_MSG:
                wpa_printf(MSG_DEBUG, "BROADCOM: GTK MIC MSG MESSAGE");
                data.michael_mic_failure.unicast = 0;
                wpa_supplicant_event(ctx, EVENT_MICHAEL_MIC_FAILURE, &data);
                break;
        default:
                wpa_printf(MSG_DEBUG, "BROADCOM: UNKNOWN MESSAGE (%d)",
                           wwh->type);
                break;
        }
        os_free(data.assoc_info.resp_ies);
}        

static void * wpa_driver_broadcom_init(void *ctx, const char *ifname)
{
        int s;
        struct sockaddr_ll ll;
        struct wpa_driver_broadcom_data *drv;
        struct ifreq ifr;

        /* open socket to kernel */
        if ((s = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
                perror("socket");
                return NULL;
        }
        /* do it */
        os_strlcpy(ifr.ifr_name, ifname, IFNAMSIZ);
        if (ioctl(s, SIOCGIFINDEX, &ifr) < 0) {
                perror(ifr.ifr_name);
                return NULL;
        }


        drv = os_zalloc(sizeof(*drv));
        if (drv == NULL)
                return NULL;
        drv->ctx = ctx;
        os_strlcpy(drv->ifname, ifname, sizeof(drv->ifname));
        drv->ioctl_sock = s;

        s = socket(PF_PACKET, SOCK_RAW, ntohs(ETH_P_802_2));
        if (s < 0) {
                perror("socket(PF_PACKET, SOCK_RAW, ntohs(ETH_P_802_2))");
                close(drv->ioctl_sock);
                os_free(drv);
                return NULL;
        }

        os_memset(&ll, 0, sizeof(ll));
        ll.sll_family = AF_PACKET;
        ll.sll_protocol = ntohs(ETH_P_802_2);
        ll.sll_ifindex = ifr.ifr_ifindex;
        ll.sll_hatype = 0;
        ll.sll_pkttype = PACKET_HOST;
        ll.sll_halen = 0;

        if (bind(s, (struct sockaddr *) &ll, sizeof(ll)) < 0) {
                perror("bind(netlink)");
                close(s);
                close(drv->ioctl_sock);
                os_free(drv);
                return NULL;
        }

        eloop_register_read_sock(s, wpa_driver_broadcom_event_receive, ctx,
                                 NULL);
        drv->event_sock = s;

        return drv;
}

static void wpa_driver_broadcom_deinit(void *priv)
{
        struct wpa_driver_broadcom_data *drv = priv;
        eloop_cancel_timeout(wpa_driver_broadcom_scan_timeout, drv, drv->ctx);
        eloop_unregister_read_sock(drv->event_sock);
        close(drv->event_sock);
        close(drv->ioctl_sock);
        os_free(drv);
}

static int wpa_driver_broadcom_set_countermeasures(void *priv,
                                                   int enabled)
{
#if 0
        struct wpa_driver_broadcom_data *drv = priv;
        /* FIX: ? */
        return broadcom_ioctl(drv, WLC_SET_TKIP_COUNTERMEASURES, &enabled,
                              sizeof(enabled));
#else
        return 0;
#endif
}

static int wpa_driver_broadcom_set_drop_unencrypted(void *priv, int enabled)
{
        struct wpa_driver_broadcom_data *drv = priv;
        /* SET_EAP_RESTRICT, SET_WEP_RESTRICT */
        int restrict = (enabled ? 1 : 0);
        
        if (broadcom_ioctl(drv, WLC_SET_WEP_RESTRICT, 
                           &restrict, sizeof(restrict)) < 0 ||
            broadcom_ioctl(drv, WLC_SET_EAP_RESTRICT,
                           &restrict, sizeof(restrict)) < 0)
                return -1;

        return 0;
}

static void wpa_driver_broadcom_scan_timeout(void *eloop_ctx,
                                             void *timeout_ctx)
{
        wpa_printf(MSG_DEBUG, "Scan timeout - try to get results");
        wpa_supplicant_event(timeout_ctx, EVENT_SCAN_RESULTS, NULL);
}

static int wpa_driver_broadcom_scan(void *priv, const u8 *ssid,
                                    size_t ssid_len)
{
        struct wpa_driver_broadcom_data *drv = priv;
        wlc_ssid_t wst = { 0, "" };

        if (ssid && ssid_len > 0 && ssid_len <= sizeof(wst.SSID)) {
                wst.SSID_len = ssid_len;
                os_memcpy(wst.SSID, ssid, ssid_len);
        }
        
        if (broadcom_ioctl(drv, WLC_SCAN, &wst, sizeof(wst)) < 0)
                return -1;

        eloop_cancel_timeout(wpa_driver_broadcom_scan_timeout, drv, drv->ctx);
        eloop_register_timeout(3, 0, wpa_driver_broadcom_scan_timeout, drv,
                               drv->ctx);
        return 0;
}


static const int frequency_list[] = { 
        2412, 2417, 2422, 2427, 2432, 2437, 2442,
        2447, 2452, 2457, 2462, 2467, 2472, 2484 
};

struct bss_ie_hdr {
        u8 elem_id;
        u8 len;
        u8 oui[3];
        /* u8 oui_type; */
        /* u16 version; */
} __attribute__ ((packed));

static int
wpa_driver_broadcom_get_scan_results(void *priv,
                                     struct wpa_scan_result *results,
                                     size_t max_size)
{
        struct wpa_driver_broadcom_data *drv = priv;
        char *buf;
        wl_scan_results_t *wsr;
        wl_bss_info_t *wbi;
        size_t ap_num;

        buf = os_malloc(WLC_IOCTL_MAXLEN);
        if (buf == NULL)
                return -1;

        wsr = (wl_scan_results_t *) buf;

        wsr->buflen = WLC_IOCTL_MAXLEN - sizeof(wsr);
        wsr->version = 107;
        wsr->count = 0;

        if (broadcom_ioctl(drv, WLC_SCAN_RESULTS, buf, WLC_IOCTL_MAXLEN) < 0) {
                os_free(buf);
                return -1;
        }

        os_memset(results, 0, max_size * sizeof(struct wpa_scan_result));

        for (ap_num = 0, wbi = wsr->bss_info; ap_num < wsr->count; ++ap_num) {
                int left;
                struct bss_ie_hdr *ie;
                
                os_memcpy(results[ap_num].bssid, &wbi->BSSID, ETH_ALEN);
                os_memcpy(results[ap_num].ssid, wbi->SSID, wbi->SSID_len);
                results[ap_num].ssid_len = wbi->SSID_len;
                results[ap_num].freq = frequency_list[wbi->channel - 1];
                /* get ie's */
                wpa_hexdump(MSG_MSGDUMP, "BROADCOM: AP IEs",
                            (u8 *) wbi + sizeof(*wbi), wbi->ie_length);
                ie = (struct bss_ie_hdr *) ((u8 *) wbi + sizeof(*wbi));
                for (left = wbi->ie_length; left > 0;
                     left -= (ie->len + 2), ie = (struct bss_ie_hdr *)
                             ((u8 *) ie + 2 + ie->len)) {
                        wpa_printf(MSG_MSGDUMP, "BROADCOM: IE: id:%x, len:%d",
                                   ie->elem_id, ie->len);
                        if (ie->len >= 3) 
                                wpa_printf(MSG_MSGDUMP,
                                           "BROADCOM: oui:%02x%02x%02x",
                                           ie->oui[0], ie->oui[1], ie->oui[2]);
                        if (ie->elem_id != 0xdd ||
                            ie->len < 6 ||
                            os_memcmp(ie->oui, WPA_OUI, 3) != 0)
                                continue;
                        os_memcpy(results[ap_num].wpa_ie, ie, ie->len + 2);
                        results[ap_num].wpa_ie_len = ie->len + 2;
                        break;
                }

                wbi = (wl_bss_info_t *) ((u8 *) wbi + wbi->length);
        }

        wpa_printf(MSG_MSGDUMP, "Received %d bytes of scan results (%lu "
                   "BSSes)",
                   wsr->buflen, (unsigned long) ap_num);
        
        os_free(buf);
        return ap_num;
}

static int wpa_driver_broadcom_deauthenticate(void *priv, const u8 *addr,
                                              int reason_code)
{
        struct wpa_driver_broadcom_data *drv = priv;
        wlc_deauth_t wdt;
        wdt.val = reason_code;
        os_memcpy(&wdt.ea, addr, sizeof wdt.ea);
        wdt.res = 0x7fff;
        return broadcom_ioctl(drv, WLC_DEAUTHENTICATE_WITH_REASON, &wdt,
                              sizeof(wdt));
}

static int wpa_driver_broadcom_disassociate(void *priv, const u8 *addr,
                                            int reason_code)
{
        struct wpa_driver_broadcom_data *drv = priv;
        return broadcom_ioctl(drv, WLC_DISASSOC, 0, 0);
}

static int
wpa_driver_broadcom_associate(void *priv,
                              struct wpa_driver_associate_params *params)
{
        struct wpa_driver_broadcom_data *drv = priv;
        wlc_ssid_t s;
        int infra = 1;
        int auth = 0;
        int wsec = 4;
        int dummy;
        int wpa_auth;
        
        s.SSID_len = params->ssid_len;
        os_memcpy(s.SSID, params->ssid, params->ssid_len);

        switch (params->pairwise_suite) {
        case CIPHER_WEP40:
        case CIPHER_WEP104:
                wsec = 1;
                break;

        case CIPHER_TKIP:
                wsec = 2;
                break;

        case CIPHER_CCMP:
                wsec = 4;
                break;

        default:
                wsec = 0;
                break;
        }

        switch (params->key_mgmt_suite) {
        case KEY_MGMT_802_1X:
                wpa_auth = 1;
                break;

        case KEY_MGMT_PSK:
                wpa_auth = 2;
                break;

        default:
                wpa_auth = 255;
                break;
        }

        /* printf("broadcom_associate: %u %u %u\n", pairwise_suite,
         * group_suite, key_mgmt_suite);
         * broadcom_ioctl(ifname, WLC_GET_WSEC, &wsec, sizeof(wsec));
         * wl join uses wlc_sec_wep here, not wlc_set_wsec */

        if (broadcom_ioctl(drv, WLC_SET_WSEC, &wsec, sizeof(wsec)) < 0 ||
            broadcom_ioctl(drv, WLC_SET_WPA_AUTH, &wpa_auth,
                           sizeof(wpa_auth)) < 0 ||
            broadcom_ioctl(drv, WLC_GET_WEP, &dummy, sizeof(dummy)) < 0 ||
            broadcom_ioctl(drv, WLC_SET_INFRA, &infra, sizeof(infra)) < 0 ||
            broadcom_ioctl(drv, WLC_SET_AUTH, &auth, sizeof(auth)) < 0 ||
            broadcom_ioctl(drv, WLC_SET_WEP, &wsec, sizeof(wsec)) < 0 ||
            broadcom_ioctl(drv, WLC_SET_SSID, &s, sizeof(s)) < 0)
                return -1;

        return 0;
}

const struct wpa_driver_ops wpa_driver_broadcom_ops = {
        .name = "broadcom",
        .desc = "Broadcom wl.o driver",
        .get_bssid = wpa_driver_broadcom_get_bssid,
        .get_ssid = wpa_driver_broadcom_get_ssid,
        .set_wpa = wpa_driver_broadcom_set_wpa,
        .set_key = wpa_driver_broadcom_set_key,
        .init = wpa_driver_broadcom_init,
        .deinit = wpa_driver_broadcom_deinit,
        .set_countermeasures = wpa_driver_broadcom_set_countermeasures,
        .set_drop_unencrypted = wpa_driver_broadcom_set_drop_unencrypted,
        .scan = wpa_driver_broadcom_scan,
        .get_scan_results = wpa_driver_broadcom_get_scan_results,
        .deauthenticate = wpa_driver_broadcom_deauthenticate,
        .disassociate = wpa_driver_broadcom_disassociate,
        .associate = wpa_driver_broadcom_associate,
};
